package edu.gmu.projects.fireresponder.comm;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.net.NetworkInterface;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.util.concurrent.LinkedBlockingQueue;

import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;

import android.content.Context;
import android.net.wifi.WifiManager;
import android.util.Log;
import edu.gmu.projects.fireresponder.xml.alerts.Alert;
import edu.gmu.projects.fireresponder.xml.alerts.AlertFactory;
import edu.gmu.projects.fireresponder.xml.alerts.FirstResponderAlert;
import edu.gmu.projects.fireresponder.xml.alerts.ResponderLocationAlert;

public class UDPBroadcast extends MessageCommunicator {

	protected InetAddress group;
	private MulticastSocket socket;

	protected String address;
	protected int port;
	protected int timeToLive;
	protected Context context;
	protected TextEncryptor textEncryptor;

	protected LinkedBlockingQueue<String> messageQueue;
	
	private boolean initialized = false;

	private static UDPBroadcast singleton;

	// package scope, initialized by UDPBroadcastService
	static UDPBroadcast createUDPBroadcast(Context context, String address,
			int port) {
		singleton = new UDPBroadcast(context, address, port);
		return singleton;
	}

	// package scope, initialized by UDPBroadcastService
	static UDPBroadcast createUDPBroadcast(Context context, String address,
			int port, int timeToLive) {
		singleton = new UDPBroadcast(context, address, port, timeToLive);
		return singleton;
	}

	// warning! may not yet have been initialized by service
	public static UDPBroadcast getUDPBroadcast() {
		return singleton;
	}

	private UDPBroadcast(Context context, String address, int port) {
		this.context = context;
		init(address, port, 1);
	}

	private UDPBroadcast(Context context, String address, int port,
			int timeToLive) {
		this.context = context;
		init(address, port, timeToLive);
	}

	private void init(String address, int port, int timeToLive) {
		this.address = address;
		this.port = port;
		messageQueue = new LinkedBlockingQueue<String>();

		if (timeToLive > 255)
			this.timeToLive = 255;
		if (timeToLive < 1)
			this.timeToLive = 1;
		else
			this.timeToLive = timeToLive;

		try {
			group = InetAddress.getByName(address);
			socket = new MulticastSocket(port);
			socket.setTimeToLive(timeToLive);

			socket.setReuseAddress(true);
			WifiManager wm = (WifiManager) context
					.getSystemService(Context.WIFI_SERVICE);
			NetworkInterface networkInterface = NetworkInterfaceUtil
					.getWifiNetworkInterface(wm);

			if (networkInterface != null) {
				socket.setNetworkInterface(networkInterface);
			}
			socket.joinGroup(group);
			textEncryptor = TextEncryptor.getTextEncryptor();
			this.encryptMessages(true);
			// this.encryptMessages(false);
			initialized = true;
			try {
				startListener();
			} catch (URISyntaxException e) {
				e.printStackTrace();
			}
			startMessageHandler();
		} catch (UnknownHostException e) {
			Log.d("-----udp", "UnknownHostException creating UDPBroadcast: "
					+ e.getMessage());
			// log.error("UnknownHostException creating UDPBroadcast: " +
			// e.getMessage());
		} catch (IOException e) {
			Log.d("-----udp", "IOException creating UDPBroadcast: "
					+ e.getMessage());
			// log.error("IOException creating UDPBroadcast: " +
			// e.getMessage());
		}
	}

	public boolean isInitialized() {
		return initialized;
	}

	protected void finalize() throws Throwable {
		try {
			socket.leaveGroup(group);
		} catch (IOException e) {
			Log.d("-----udp", "IOException disposing of UDPBroadcast: "
					+ e.getMessage());
			// log.error("IOException disposing of UDPBroadcast: " +
			// e.getMessage());
		} catch (Exception e) {
			Log.d("-----udp", e.getClass().getName()
					+ " disposing of UDPBroadcast: " + e.getMessage());
			// log.error(e.getClass().getName() + " disposing of UDPBroadcast: "
			// + e.getMessage());
		} finally {
			super.finalize();
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.lmco.atl.swtp.network.Broadcast#send(com.lmco.atl.swtp.network.String
	 * )
	 */
	public boolean send(String msg) {
		Log.d("-----udp", "Beginning");

		try {
			synchronized(this) {
				ByteArrayOutputStream stream = new ByteArrayOutputStream();
				ObjectOutputStream oos = null;
				if (this.isEncrypted()) {
					CipherOutputStream cos = new CipherOutputStream(stream,
						textEncryptor.getEncryptionCipher());
					oos = new ObjectOutputStream(cos);
				} else {
					oos = new ObjectOutputStream(stream);
				}
				oos.writeObject(msg);
				oos.close();

				byte[] bytes = stream.toByteArray();
				DatagramPacket packet = new DatagramPacket(bytes, bytes.length,
					getHost(), getPort());
				getSocket().send(packet);

				Log.d("-----udp", "Message successfully sent");
			}
		} catch (NullPointerException e) {
			Log.d("-----udp", "NullPointerException sending UDP String: "
					+ e.getMessage());
			return false;
		} catch (IOException e) {
			Log.d("-----udp", "IOException sending UDP String: "
					+ e.getMessage());
			return false;
		}
		Log.d("-----udp", "End");
		return true;
	}

	protected void startListener() throws URISyntaxException {

		Thread thread = new Thread(new SocketListener(this), "UDP Listener");
		thread.start();
		Thread.yield();

		/*
		 * String task = new String("android 1"); send(task); Log.i("-----Sent",
		 * task); task = new String("android 2"); send(task); Log.i("-----Sent",
		 * task); task = new String("android 3"); send(task); Log.i("-----Sent",
		 * task); task = new String("android 4"); send(task); Log.i("-----Sent",
		 * task);
		 */
	}

	protected void startMessageHandler() {
		Thread thread = new Thread(new MessageHandler(this), "Message Listener");
		thread.start();
		Thread.yield();		
	}
	/*
	 * (non-Javadoc)
	 * 
	 * @see com.lmco.atl.swtp.network.Broadcast#receive()
	 */
	public String receive() {
		byte[] buf = new byte[1048576];
		String message = null;
		DatagramPacket recv = new DatagramPacket(buf, buf.length);

		try {
			getSocket().receive(recv);
			ByteArrayInputStream stream = new ByteArrayInputStream(buf);
			ObjectInputStream ois = null;

			if (isEncrypted()) {
				CipherInputStream cos = new CipherInputStream(stream,
						textEncryptor.getDecryptionCipher());
				ois = new ObjectInputStream(cos);
			} else {
				ois = new ObjectInputStream(stream);
			}
			message = (String) ois.readObject();
			ois.close();
		} catch (NullPointerException e) {
			Log.d("-----udp", "NullPointerException receiving UDP String: "
					+ e.getMessage());
		} catch (IOException e) {
			Log.d("-----udp", "IOException receiving UDP String: "
					+ e.getMessage());
		} catch (ClassNotFoundException e) {
			Log.d("-----udp", "ClassNotFoundException receiving UDP String: "
					+ e.getMessage());
		}

		return message;
	}

	protected class MessageHandler implements Runnable {

		private UDPBroadcast bc;

		public MessageHandler(UDPBroadcast bc) {
			this.bc = bc;
		}

		public void run() {
			while(true) {
				String msg = null;
				try {
					msg = bc.messageQueue.take();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}

				if (msg != null) {
					Alert alert = AlertFactory.createAlertFromMessage(msg);

					if (alert != null) {
						for (MessageListener listener : listeners) {
							listener.receivedAlertMessage(alert);
						}

						if (alert instanceof FirstResponderAlert) {
							for (IFirstResponderAlertListener l : frListeners) {
								FirstResponderAlert fa = (FirstResponderAlert) alert;
								l.receivedFirstResponderAlert(fa);
							}
						} else if (alert instanceof ResponderLocationAlert) {
							for (IFirstResponderAlertListener l : frListeners) {
								ResponderLocationAlert fa = (ResponderLocationAlert) alert;
								l.receivedResponderLocationAlert(fa);
							}
						}
					}
				}

			}
		}		
	}
	
	protected class SocketListener implements Runnable {

		private UDPBroadcast bc;

		public SocketListener(UDPBroadcast bc) {
			this.bc = bc;
		}

		public void run() {
			while (true) {
				String msg = bc.receive();
				if (msg == null) {
					continue;
				}
				msg = msg.trim();
				bc.messageQueue.add(msg);

				Log.i("-----Received", msg);

			}
		}
	}

	/**
	 * @return the socket
	 */
	protected DatagramSocket getSocket() {
		return socket;
	}

	protected InetAddress getHost() {
		return group;
	}

	protected int getPort() {
		return port;
	}

}
